// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;

namespace System.Diagnostics.Tracing
{
    /// <summary>
    /// TraceLogging: Constants and utility functions.
    /// </summary>
    internal static class Statics
    {
        #region Constants

        public const byte DefaultLevel = 5;
        public const byte TraceLoggingChannel = 0xb;
        public const byte InTypeMask = 31;
        public const byte InTypeFixedCountFlag = 32;
        public const byte InTypeVariableCountFlag = 64;
        public const byte InTypeCustomCountFlag = 96;
        public const byte InTypeCountMask = 96;
        public const byte InTypeChainFlag = 128;
        public const byte OutTypeMask = 127;
        public const byte OutTypeChainFlag = 128;
        public const EventTags EventTagsMask = (EventTags)0xfffffff;

        public static readonly TraceLoggingDataType IntPtrType = IntPtr.Size == 8
            ? TraceLoggingDataType.Int64
            : TraceLoggingDataType.Int32;
        public static readonly TraceLoggingDataType UIntPtrType = IntPtr.Size == 8
            ? TraceLoggingDataType.UInt64
            : TraceLoggingDataType.UInt32;
        public static readonly TraceLoggingDataType HexIntPtrType = IntPtr.Size == 8
            ? TraceLoggingDataType.HexInt64
            : TraceLoggingDataType.HexInt32;

        #endregion

        #region Metadata helpers

        /// <summary>
        /// A complete metadata chunk can be expressed as:
        /// length16 + prefix + null-terminated-utf8-name + suffix + additionalData.
        /// We assume that excludedData will be provided by some other means,
        /// but that its size is known. This function returns a blob containing
        /// length16 + prefix + name + suffix, with prefix and suffix initialized
        /// to 0's. The length16 value is initialized to the length of the returned
        /// blob plus additionalSize, so that the concatenation of the returned blob
        /// plus a blob of size additionalSize constitutes a valid metadata blob.
        /// </summary>
        /// <param name="name">
        /// The name to include in the blob.
        /// </param>
        /// <param name="prefixSize">
        /// Amount of space to reserve before name. For provider or field blobs, this
        /// should be 0. For event blobs, this is used for the tags field and will vary
        /// from 1 to 4, depending on how large the tags field needs to be.
        /// </param>
        /// <param name="suffixSize">
        /// Amount of space to reserve after name. For example, a provider blob with no
        /// traits would reserve 0 extra bytes, but a provider blob with a single GroupId
        /// field would reserve 19 extra bytes.
        /// </param>
        /// <param name="additionalSize">
        /// Amount of additional data in another blob. This value will be counted in the
        /// blob's length field, but will not be included in the returned byte[] object.
        /// The complete blob would then be the concatenation of the returned byte[] object
        /// with another byte[] object of length additionalSize.
        /// </param>
        /// <returns>
        /// A byte[] object with the length and name fields set, with room reserved for
        /// prefix and suffix. If additionalSize was 0, the byte[] object is a complete
        /// blob. Otherwise, another byte[] of size additionalSize must be concatenated
        /// with this one to form a complete blob.
        /// </returns>
        public static byte[] MetadataForString(
            string name,
            int prefixSize,
            int suffixSize,
            int additionalSize)
        {
            CheckName(name);
            int metadataSize = Encoding.UTF8.GetByteCount(name) + 3 + prefixSize + suffixSize;
            var metadata = new byte[metadataSize];
            ushort totalSize = checked((ushort)(metadataSize + additionalSize));
            metadata[0] = unchecked((byte)totalSize);
            metadata[1] = unchecked((byte)(totalSize >> 8));
            Encoding.UTF8.GetBytes(name, 0, name.Length, metadata, 2 + prefixSize);
            return metadata;
        }

        /// <summary>
        /// Serialize the low 28 bits of the tags value into the metadata stream,
        /// starting at the index given by pos. Updates pos. Writes 1 to 4 bytes,
        /// depending on the value of the tags variable. Usable for event tags and
        /// field tags.
        ///
        /// Note that 'metadata' can be null, in which case it only updates 'pos'.
        /// This is useful for a two pass approach where you figure out how big to
        /// make the array, and then you fill it in.
        /// </summary>
        public static void EncodeTags(int tags, ref int pos, byte[]? metadata)
        {
            // We transmit the low 28 bits of tags, high bits first, 7 bits at a time.
            int tagsLeft = tags & 0xfffffff;
            bool more;
            do
            {
                byte current = (byte)((tagsLeft >> 21) & 0x7f);
                more = (tagsLeft & 0x1fffff) != 0;
                current |= (byte)(more ? 0x80 : 0x00);
                tagsLeft <<= 7;

                metadata?[pos] = current;
                pos++;
            }
            while (more);
        }

        public static byte Combine(
            int settingValue,
            byte defaultValue)
        {
            unchecked
            {
                return (byte)settingValue == settingValue
                    ? (byte)settingValue
                    : defaultValue;
            }
        }

        public static byte Combine(
            int settingValue1,
            int settingValue2,
            byte defaultValue)
        {
            unchecked
            {
                return (byte)settingValue1 == settingValue1
                    ? (byte)settingValue1
                    : (byte)settingValue2 == settingValue2
                    ? (byte)settingValue2
                    : defaultValue;
            }
        }

        public static int Combine(
            int settingValue1,
            int settingValue2)
        {
            unchecked
            {
                return (byte)settingValue1 == settingValue1
                    ? settingValue1
                    : settingValue2;
            }
        }

        public static void CheckName(string? name)
        {
            if (name != null && 0 <= name.IndexOf('\0'))
            {
                throw new ArgumentOutOfRangeException(nameof(name));
            }
        }

        public static bool ShouldOverrideFieldName(string fieldName)
        {
            return fieldName.Length <= 2 && fieldName[0] == '_';
        }

        public static TraceLoggingDataType MakeDataType(
            TraceLoggingDataType baseType,
            EventFieldFormat format)
        {
            return (TraceLoggingDataType)(((int)baseType & 0x1f) | ((int)format << 8));
        }

        /// <summary>
        /// Adjusts the native type based on format.
        /// - If format is default, return native.
        /// - If format is recognized, return the canonical type for that format.
        /// - Otherwise remove existing format from native and apply the requested format.
        /// </summary>
        public static TraceLoggingDataType Format8(
            EventFieldFormat format,
            TraceLoggingDataType native)
        {
            return format switch
            {
                EventFieldFormat.Default => native,
                EventFieldFormat.String => TraceLoggingDataType.Char8,
                EventFieldFormat.Boolean => TraceLoggingDataType.Boolean8,
                EventFieldFormat.Hexadecimal => TraceLoggingDataType.HexInt8,
#if false
                EventSourceFieldFormat.Signed => TraceLoggingDataType.Int8,
                EventSourceFieldFormat.Unsigned => TraceLoggingDataType.UInt8,
#endif
                _ => MakeDataType(native, format),
            };
        }

        /// <summary>
        /// Adjusts the native type based on format.
        /// - If format is default, return native.
        /// - If format is recognized, return the canonical type for that format.
        /// - Otherwise remove existing format from native and apply the requested format.
        /// </summary>
        public static TraceLoggingDataType Format16(
            EventFieldFormat format,
            TraceLoggingDataType native)
        {
            return format switch
            {
                EventFieldFormat.Default => native,
                EventFieldFormat.String => TraceLoggingDataType.Char16,
                EventFieldFormat.Hexadecimal => TraceLoggingDataType.HexInt16,
#if false
                EventSourceFieldFormat.Port => TraceLoggingDataType.Port,
                EventSourceFieldFormat.Signed => TraceLoggingDataType.Int16,
                EventSourceFieldFormat.Unsigned => TraceLoggingDataType.UInt16,
#endif
                _ => MakeDataType(native, format),
            };
        }

        /// <summary>
        /// Adjusts the native type based on format.
        /// - If format is default, return native.
        /// - If format is recognized, return the canonical type for that format.
        /// - Otherwise remove existing format from native and apply the requested format.
        /// </summary>
        public static TraceLoggingDataType Format32(
            EventFieldFormat format,
            TraceLoggingDataType native)
        {
            return format switch
            {
                EventFieldFormat.Default => native,
                EventFieldFormat.Boolean => TraceLoggingDataType.Boolean32,
                EventFieldFormat.Hexadecimal => TraceLoggingDataType.HexInt32,
#if false
                EventSourceFieldFormat.Ipv4Address => TraceLoggingDataType.Ipv4Address,
                EventSourceFieldFormat.ProcessId => TraceLoggingDataType.ProcessId,
                EventSourceFieldFormat.ThreadId => TraceLoggingDataType.ThreadId,
                EventSourceFieldFormat.Win32Error => TraceLoggingDataType.Win32Error,
                EventSourceFieldFormat.NTStatus => TraceLoggingDataType.NTStatus,
#endif
                EventFieldFormat.HResult => TraceLoggingDataType.HResult,
#if false
                case EventSourceFieldFormat.Signed:
                    return TraceLoggingDataType.Int32;
                case EventSourceFieldFormat.Unsigned:
                    return TraceLoggingDataType.UInt32;
#endif
                _ => MakeDataType(native, format),
            };
        }

        /// <summary>
        /// Adjusts the native type based on format.
        /// - If format is default, return native.
        /// - If format is recognized, return the canonical type for that format.
        /// - Otherwise remove existing format from native and apply the requested format.
        /// </summary>
        public static TraceLoggingDataType Format64(
            EventFieldFormat format,
            TraceLoggingDataType native)
        {
            return format switch
            {
                EventFieldFormat.Default => native,
                EventFieldFormat.Hexadecimal => TraceLoggingDataType.HexInt64,
#if false
                EventSourceFieldFormat.FileTime => TraceLoggingDataType.FileTime,
                EventSourceFieldFormat.Signed => TraceLoggingDataType.Int64,
                EventSourceFieldFormat.Unsigned => TraceLoggingDataType.UInt64,
#endif
                _ => MakeDataType(native, format),
            };
        }

        /// <summary>
        /// Adjusts the native type based on format.
        /// - If format is default, return native.
        /// - If format is recognized, return the canonical type for that format.
        /// - Otherwise remove existing format from native and apply the requested format.
        /// </summary>
        public static TraceLoggingDataType FormatPtr(
            EventFieldFormat format,
            TraceLoggingDataType native)
        {
            return format switch
            {
                EventFieldFormat.Default => native,
                EventFieldFormat.Hexadecimal => HexIntPtrType,
#if false
                EventSourceFieldFormat.Signed => IntPtrType,
                EventSourceFieldFormat.Unsigned => UIntPtrType,
#endif
                _ => MakeDataType(native, format),
            };
        }

        public static TraceLoggingDataType FormatScalar(EventFieldFormat format, TraceLoggingDataType nativeFormat) =>
            nativeFormat switch
            {
                TraceLoggingDataType.Boolean8 or TraceLoggingDataType.Int8 or TraceLoggingDataType.UInt8 => Format8(format, nativeFormat),
                TraceLoggingDataType.Char16 or TraceLoggingDataType.Int16 or TraceLoggingDataType.UInt16 => Format16(format, nativeFormat),
                TraceLoggingDataType.Int32 or TraceLoggingDataType.UInt32 or TraceLoggingDataType.Float => Format32(format, nativeFormat),
                TraceLoggingDataType.Int64 or TraceLoggingDataType.UInt64 or TraceLoggingDataType.Double => Format64(format, nativeFormat),
                _ => MakeDataType(nativeFormat, format),
            };

        #endregion

        #region Reflection helpers

        public static bool HasCustomAttribute(
            PropertyInfo propInfo,
            Type attributeType)
        {
            return propInfo.IsDefined(attributeType, false);
        }

        public static AttributeType? GetCustomAttribute<AttributeType>(PropertyInfo propInfo)
            where AttributeType : Attribute
        {
            AttributeType? result = null;
            object[] attributes = propInfo.GetCustomAttributes(typeof(AttributeType), false);
            if (attributes.Length != 0)
            {
                result = (AttributeType)attributes[0];
            }
            return result;
        }

        public static AttributeType? GetCustomAttribute<AttributeType>(Type type)
            where AttributeType : Attribute
        {
            AttributeType? result = null;
            object[] attributes = type.GetCustomAttributes(typeof(AttributeType), false);
            if (attributes.Length != 0)
            {
                result = (AttributeType)attributes[0];
            }
            return result;
        }

        public static Type? FindEnumerableElementType(
            [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
            Type type)
        {
            Type? elementType = null;

            if (IsGenericMatch(type, typeof(IEnumerable<>)))
            {
                elementType = type.GetGenericArguments()[0];
            }
            else
            {
                Type[] ifaceTypes = type.FindInterfaces(IsGenericMatch, typeof(IEnumerable<>));

                foreach (Type ifaceType in ifaceTypes)
                {
                    if (elementType != null)
                    {
                        // ambiguous match. report no match at all.
                        elementType = null;
                        break;
                    }

                    elementType = ifaceType.GetGenericArguments()[0];
                }
            }

            return elementType;
        }

        public static bool IsGenericMatch(Type type, object? openType)
        {
            return type.IsGenericType && type.GetGenericTypeDefinition() == (Type?)openType;
        }

        [RequiresUnreferencedCode("EventSource WriteEvent will serialize the whole object graph. Trimmer will not safely handle this case because properties may be trimmed. This can be suppressed if the object is a primitive type")]
        public static TraceLoggingTypeInfo CreateDefaultTypeInfo(
            Type dataType,
            List<Type> recursionCheck)
        {
            TraceLoggingTypeInfo result;

            if (recursionCheck.Contains(dataType))
            {
                throw new NotSupportedException(SR.EventSource_RecursiveTypeDefinition);
            }

            recursionCheck.Add(dataType);

            EventDataAttribute? eventAttrib = GetCustomAttribute<EventDataAttribute>(dataType);
            if (eventAttrib != null ||
                GetCustomAttribute<CompilerGeneratedAttribute>(dataType) != null ||
                IsGenericMatch(dataType, typeof(KeyValuePair<,>)))
            {
                var analysis = new TypeAnalysis(dataType, eventAttrib, recursionCheck);
                result = new InvokeTypeInfo(dataType, analysis);
            }
            else if (dataType.IsArray)
            {
                Type elementType = dataType.GetElementType()!;
                if (elementType == typeof(bool))
                {
                    result = ScalarArrayTypeInfo.Boolean();
                }
                else if (elementType == typeof(byte))
                {
                    result = ScalarArrayTypeInfo.Byte();
                }
                else if (elementType == typeof(sbyte))
                {
                    result = ScalarArrayTypeInfo.SByte();
                }
                else if (elementType == typeof(short))
                {
                    result = ScalarArrayTypeInfo.Int16();
                }
                else if (elementType == typeof(ushort))
                {
                    result = ScalarArrayTypeInfo.UInt16();
                }
                else if (elementType == typeof(int))
                {
                    result = ScalarArrayTypeInfo.Int32();
                }
                else if (elementType == typeof(uint))
                {
                    result = ScalarArrayTypeInfo.UInt32();
                }
                else if (elementType == typeof(long))
                {
                    result = ScalarArrayTypeInfo.Int64();
                }
                else if (elementType == typeof(ulong))
                {
                    result = ScalarArrayTypeInfo.UInt64();
                }
                else if (elementType == typeof(char))
                {
                    result = ScalarArrayTypeInfo.Char();
                }
                else if (elementType == typeof(double))
                {
                    result = ScalarArrayTypeInfo.Double();
                }
                else if (elementType == typeof(float))
                {
                    result = ScalarArrayTypeInfo.Single();
                }
                else if (elementType == typeof(IntPtr))
                {
                    result = ScalarArrayTypeInfo.IntPtr();
                }
                else if (elementType == typeof(UIntPtr))
                {
                    result = ScalarArrayTypeInfo.UIntPtr();
                }
                else if (elementType == typeof(Guid))
                {
                    result = ScalarArrayTypeInfo.Guid();
                }
                else
                {
                    result = new ArrayTypeInfo(dataType, TraceLoggingTypeInfo.GetInstance(elementType, recursionCheck));
                }
            }
            else
            {
                if (dataType.IsEnum)
                    dataType = Enum.GetUnderlyingType(dataType);

                if (dataType == typeof(string))
                {
                    result = StringTypeInfo.Instance();
                }
                else if (dataType == typeof(bool))
                {
                    result = ScalarTypeInfo.Boolean();
                }
                else if (dataType == typeof(byte))
                {
                    result = ScalarTypeInfo.Byte();
                }
                else if (dataType == typeof(sbyte))
                {
                    result = ScalarTypeInfo.SByte();
                }
                else if (dataType == typeof(short))
                {
                    result = ScalarTypeInfo.Int16();
                }
                else if (dataType == typeof(ushort))
                {
                    result = ScalarTypeInfo.UInt16();
                }
                else if (dataType == typeof(int))
                {
                    result = ScalarTypeInfo.Int32();
                }
                else if (dataType == typeof(uint))
                {
                    result = ScalarTypeInfo.UInt32();
                }
                else if (dataType == typeof(long))
                {
                    result = ScalarTypeInfo.Int64();
                }
                else if (dataType == typeof(ulong))
                {
                    result = ScalarTypeInfo.UInt64();
                }
                else if (dataType == typeof(char))
                {
                    result = ScalarTypeInfo.Char();
                }
                else if (dataType == typeof(double))
                {
                    result = ScalarTypeInfo.Double();
                }
                else if (dataType == typeof(float))
                {
                    result = ScalarTypeInfo.Single();
                }
                else if (dataType == typeof(DateTime))
                {
                    result = DateTimeTypeInfo.Instance();
                }
                else if (dataType == typeof(decimal))
                {
                    result = DecimalTypeInfo.Instance();
                }
                else if (dataType == typeof(IntPtr))
                {
                    result = ScalarTypeInfo.IntPtr();
                }
                else if (dataType == typeof(UIntPtr))
                {
                    result = ScalarTypeInfo.UIntPtr();
                }
                else if (dataType == typeof(Guid))
                {
                    result = ScalarTypeInfo.Guid();
                }
                else if (dataType == typeof(TimeSpan))
                {
                    result = TimeSpanTypeInfo.Instance();
                }
                else if (dataType == typeof(DateTimeOffset))
                {
                    result = DateTimeOffsetTypeInfo.Instance();
                }
                else if (dataType == typeof(EmptyStruct))
                {
                    result = NullTypeInfo.Instance();
                }
                else if (IsGenericMatch(dataType, typeof(Nullable<>)))
                {
                    result = new NullableTypeInfo(dataType, recursionCheck);
                }
                else
                {
                    Type? elementType = FindEnumerableElementType(dataType);
                    if (elementType != null)
                    {
                        result = new EnumerableTypeInfo(dataType, TraceLoggingTypeInfo.GetInstance(elementType, recursionCheck));
                    }
                    else
                    {
                        throw new ArgumentException(SR.Format(SR.EventSource_NonCompliantTypeError, dataType.Name));
                    }
                }
            }

            return result;
        }

        #endregion
    }
}
